昨日直接地閱讀 ELF 檔而找到這些重定的樣貌,今天我們來設法將這些資訊轉給 readelf,藉此累積一些重定內容的操作方式吧!
由於我們時間也不多了,所以筆者預計只做一個 R_RISCV_CALL 的重定。為此,接下來的實作內容將分別是:
由於目標很明確,所以我們也無須挑戰之前總是用來當作範例的 main.o 檔,它將來必須能夠與動態函式庫連結,同時又有諸多重定型態。筆者這裡重新設計一個測試程式,也當作本系列文最後的測試程式。這個測試程式也不是重新撰寫,而是將之前的 static 程式拆分成主函式 _start 與負責輸出的 write 函式的部份。
首先是進入點的 _start 函式所在的 fin2.s:
$ cat fin2.s
.section .text
_start:
.global _start
addi sp, sp, -16
lui t2, 0x44434
addi t2, t2, 577
sw t2, 0(sp)
add a0, zero, sp
call write
addi a0, zero, 0
addi a7, zero, 93
ecall
.end
各位讀者可以注意到這裡 lui 指令的整數部加上了 0x 字首,這是之前我們的 as 還沒有支援的語法;因為之前我們是基於 objdump -d 之後的結果當作唯一支援的格式,所以整數部分會直接當作 16 進位數字處理。但是目前我們還沒有開始在組譯器 as 實作對應的功能,而必須使用 GNU 的 as,他們必須要有這些前綴才能夠正確判讀數字。
這裡將 t2 的內容存入 0x44434241 的整數,也就是字串 ABCD 的 ASCII 碼。之後我們將這個位置放在 a0 暫存器傳入 write 函式。最後的三行則是一個程式的職責:呼叫 exit 系列的系統呼叫來正常的結束。
再來是 write 函式正式定義的 fin1.s:
$ cat fin1.s
.section .text
write:
.global write
addi sp, sp, -16
sd ra, 8(sp)
sd s0, 0(sp)
addi s0, sp, 16
add a1, zero, a0
addi a0, zero, 1
addi a2, zero, 4
addi a7, zero, 64
ecall
ld ra, 8(sp)
ld s0, 0(sp)
addi sp, sp, 16
jalr zero, 0(ra)
.end
必須注意 .global 的組譯器選項萬萬短少不得,否則 write 無法被連結不說,連 _start 進入點都會被連結器以為不存在。
有個重要的觀念叫做呼叫慣例(Calling Convention)可以在這裡順便介紹。我們可以看到函式開始的地方, sp 暫存器扣除 16 的操作,這是因為程式執行期的 stack 成長方向向下,每一次呼叫都會必須儲存之後函式結束時要回到的地方,所以這裡會存入 ra 到距離原本的 sp 最近的地方。s0 暫存器的別名是 fp,也就是框架指標(frame pointer),這個資訊代表前一個函式執行時的環境的 sp 所在之處。離開時,則是將這些資訊從堆疊中提取出來,並恢復 sp 原本的值。進入時的階段叫做 function prologue,離開時則叫做 function epilogue。
當然,sp 是堆疊指標(stack pointer)的簡寫,希望大家沒有忘的太乾淨。
本體內容即是將必須的資訊傳送給 write 系統呼叫。關於這個系統呼叫,請參考筆者去年的拙作。這裡對應的是
write 函式的 a0 暫存器取得。write 系統呼叫的第三個參數,代表要印出的字元數。最後為了方便建置,這是筆者使用的 Makefile:
$ cat Makefile
AS := riscv64-unknown-linux-gnu-as
LD := riscv64-unknown-linux-gnu-ld
all: fin
fin: fin2.o fin1.o
$(LD) -o $@ $^
*.o: *.s
run:
qemu-riscv64 ./fin
因為我們的 as 還沒準備好,ld 還沒實作,所以先採用 GNU 的版本。
如同之前探索的,在 go 語言的 debug/elf 函式庫中,RISC-V 架構的重定資訊有所短少,所以這裡我們先將之補完。有兩個部份,一個是 R_RISCV 的重定型態常數變數集合:
type R_RISCV int
const (
...
R_RISCV_BRANCH R_RISCV = 16
R_RISCV_JAL R_RISCV = 17
R_RISCV_CALL R_RISCV = 18
R_RISCV_CALL_PLT R_RISCV = 19
R_RISCV_GOT_HI20 R_RISCV = 20
R_RISCV_TLS_GOT_HI20 R_RISCV = 21
R_RISCV_TLS_GD_HI20 R_RISCV = 22
R_RISCV_PCREL_HI20 R_RISCV = 23
...
另外一個則是為了輸出的目的而使用的字串
var rxRISCVStrings = []intName{
...
{16, "R_RISCV_BRANCH"},
{17, "R_RISCV_JAL"},
{18, "R_RISCV_CALL"},
{19, "R_RISCV_CALL_PLT"},
...
這些都有了之後,我們就可以開始來嘗試 readelf 了。
先來看看偉大的 GNU 版本的輸出:
$ riscv64-unknown-linux-gnu-readelf -r fin2.o
Relocation section '.rela.text' at offset 0x108 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000014 000500000012 R_RISCV_CALL 0000000000000000 write + 0
000000000014 000000000033 R_RISCV_RELAX 0
這裡就是將位在 0x14 的 call write 的虛擬指令展開成 auipc+jalr 配對,但是我們目前不支援輸出 R_RISCV_RELAX 重定型態,所以到時候應該這裡只有一組 R_RISCV_CALL。
那麼我們可以來實作 readelf 了。先看 Output 函式顯示的內容:
192 if *args["r"].(*bool) {
193 var output []elf.Rela64
194 json.Unmarshal(reu.raw["r"], &output)
195
196 fmt.Fprintln(w, "Relocation:\t\t\t")
197 fmt.Fprintln(w, "Offset\tType\tSymbol\tAppend")
198
199 syms, _ := reu.file.Symbols()
200
201 for _, s := range output {
202 if elf.R_SYM64(s.Info) == 0 {
203 continue
204 }
205
206 fmt.Fprintf(w, "%016x\t%s\t%s\t%d\n",
207 s.Off, elf.R_RISCV(elf.R_TYPE64(s.Info)).GoString(), syms[elf.R_SYM64(s.Info)-1].Name, s.Addend)
208 }
209 fmt.Fprintln(w)
210 w.Flush()
211 }
Run 函是的相對應新增則是:
108 if *args["r"].(*bool) {
109 var index int
110 for i, p := range reu.file.Sections {
111 if p.Name == ".rela.text" {
112 index = i
113 break
114 }
115 }
116
117 var rela elf.Rela64
118 str := "]"
119 var end error
120 r := reu.file.Sections[index].Open()
121 for ; end == nil; end = binary.Read(r, binary.LittleEndian, &rela) {
122
123 raw, err := json.Marshal(rela)
124 if err != nil {
125 return err
126 }
127
128 str = "," + string(raw) + str
129 }
130 re, _ := regexp.Compile("^,")
131 str = re.ReplaceAllString(str, "[")
132
133 reu.raw["r"] = []byte(str)
134 }
首先我們取得名為 .rela.text 的區段,然後透過它的 Open 方法取得一個 io.Reader,也就是讀取的對象;然後,運用 binary 函式庫的 Read 方法取得一個一個的 Rela64 結構,其餘部份則和其他的選項沒有太多差異。
最後結果類似這樣:
/tmp/readelf -r ~/fin2.o
Relocation:
Offset Type Symbol Append
0000000000000014 elf.R_RISCV_CALL write 0
今日筆者完成了 readelf 的重定擴充實作。各位讀者我們明日再會!